
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using ScrewTurn.Wiki.PluginFramework;
using System.Globalization;

namespace ScrewTurn.Wiki {

	/// <summary>
	/// Implements a Pages Storage Provider.
	/// </summary>
	[Serializable]
	public class DefaultLocalPagesStorageProvider : IPagesStorageProvider 
    {

		/// <summary>
		/// Used as lock object for multithreaded operations.
		/// </summary>
		private object locker = new object();
		private object lockerDisc = new object();
		private ComponentInformation info = new ComponentInformation("Local Pages Provider", "ScrewTurn Software", "http://www.screwturn.eu");
		private IHost host;

		// This cache is needed due to performance problems
		private PageInfo[] pagesCache = null;
		private CategoryInfo[] categoriesCache = null;

		/// <summary>
		/// Initializes the Provider.
		/// </summary>
		/// <param name="host">The Host of the Provider.</param>
		/// <param name="config">The Configuration data, if any.</param>
		public void Init(IHost host, string config) {
			this.host = host;
		}

		/// <summary>
		/// Method invoked on shutdown.
		/// </summary>
		/// <remarks>This method might not be invoked in some cases.</remarks>
		public void Shutdown() { }

		/// <summary>
		/// Gets the Information about the Provider.
		/// </summary>
		public ComponentInformation Information {
			get { return info; }
		}

		/// <summary>
		/// Gets a value specifying whether the provider is read-only, i.e. it can only provide data and not store it.
		/// </summary>
		public bool ReadOnly {
			get { return false; }
		}

		/// <summary>
		/// Gets all the Pages.
		/// </summary>
		/// <remarks>The array is unsorted.</remarks>
		public PageInfo[] AllPages {
			get {
				lock(locker) {
					if(pagesCache != null) return pagesCache;
				}
				string tmp;
				lock(locker) {
					tmp = Tools.LoadFile(Settings.PagesFile).Replace("\r", "");
				}
				string[] lines = tmp.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
				PageInfo[] result = new PageInfo[lines.Length];
				string[] fields;
				for(int i = 0; i < lines.Length; i++) {
					fields = lines[i].Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
					// Structure
					// PageName|PageFile|Status(optional);
					PageStatus ps = PageStatus.Normal;
					DateTime creationDateTime = new DateTime(2000, 1, 1);
					if(fields.Length == 2) ps = PageStatus.Normal;
					if(fields.Length >= 3) {
						switch(fields[2].ToLowerInvariant()) {
							case "locked":
								ps = PageStatus.Locked;
								break;
							case "public":
								ps = PageStatus.Public;
								break;
							case "normal":
								ps = PageStatus.Normal;
								break;
							default:
								try {
									creationDateTime = DateTime.Parse(fields[2]);
								}
								catch {
									// Use the Date/Time of the file
									FileInfo fi = new FileInfo(Settings.PagesDirectory + fields[1]);
									creationDateTime = fi.CreationTime;
								}
								break;
						}
					}
					if(fields.Length == 4) {
						creationDateTime = DateTime.Parse(fields[3]);
					}
					result[i] = new LocalPageInfo(fields[0], this, ps, creationDateTime, fields[1]);
				}
				lock(locker) {
					pagesCache = result;
				}
				return result;
			}
		}

		private bool PageExists(PageInfo page) {
			PageInfo[] pages = AllPages;
			PageNameComparer comp = new PageNameComparer();
			for(int i = 0; i < pages.Length; i++) {
				if(comp.Compare(pages[i], page) == 0) return true;
			}
			return false;
		}

		/// <summary>
		/// Gets all the Categories.
		/// </summary>
		/// <remarks>The array is unsorted.</remarks>
		public CategoryInfo[] AllCategories {
			get {
				lock(locker) {
					if(categoriesCache != null) return categoriesCache;
				}
				string tmp;
				lock(locker) {
					tmp = Tools.LoadFile(Settings.CategoriesFile).Replace("\r", "");
				}
				string[] lines = tmp.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
				CategoryInfo[] result = new CategoryInfo[lines.Length];
				string[] fields;
				for(int i = 0; i < lines.Length; i++) {
					fields = lines[i].Split('|');
					CategoryInfo cat = new CategoryInfo(fields[0], this);
					List<string> pages = new List<string>();
					for(int k = 0; k < fields.Length - 1; k++) {
						if(PageExists(new PageInfo(fields[k + 1], this, PageStatus.Normal, DateTime.Now))) {
							pages.Add(fields[k + 1]);
						}
					}
					cat.Pages = pages.ToArray();
					result[i] = cat;
				}
				lock(locker) {
					categoriesCache = result;
				}
				return result;
			}
		}

		private bool CategoryExists(CategoryInfo category) {
			CategoryInfo[] cats = AllCategories;
			CategoryNameComparer comp = new CategoryNameComparer();
			for(int i = 0; i < cats.Length; i++) {
				if(comp.Compare(cats[i], category) == 0) return true;
			}
			return false;
		}

		/// <summary>
		/// Adds a new Category.
		/// </summary>
		/// <param name="name">The Category name.</param>
		/// <returns>The correct CategoryInfo object.</returns>
		public CategoryInfo AddCategory(string name) {
			lock(locker) {
				Tools.AppendFile(Settings.CategoriesFile, "\r\n" + name);
				CategoryInfo res = new CategoryInfo(name, this);
				res.Pages = new string[0];
				categoriesCache = null;
				return res;
			}
		}

		/// <summary>
		/// Renames a Category.
		/// </summary>
		/// <param name="category">The Category to rename.</param>
		/// <param name="newName">The new Name.</param>
		/// <returns>The correct CategoryInfo object.</returns>
		public CategoryInfo RenameCategory(CategoryInfo category, string newName) {
			lock(locker) {
				CategoryInfo[] cats = AllCategories;
				CategoryNameComparer comp = new CategoryNameComparer();
				for(int i = 0; i < cats.Length; i++) {
					if(comp.Compare(cats[i], category) == 0) {
						CategoryInfo tmp = new CategoryInfo(newName, this);
						tmp.Pages = cats[i].Pages;
						cats[i] = tmp;
						DumpCategories(cats);
						categoriesCache = null;
						return tmp;
					}
				}
			}
			return null;
		}

		/// <summary>
		/// Removes a Category.
		/// </summary>
		/// <param name="category">The Category to remove.</param>
		/// <returns>True if the Category has been removed successfully.</returns>
		public bool RemoveCategory(CategoryInfo category) {
			lock(locker) {
				CategoryInfo[] cats = AllCategories;
				CategoryNameComparer comp = new CategoryNameComparer();
				for(int i = 0; i < cats.Length; i++) {
					if(comp.Compare(cats[i], category) == 0) {
						List<CategoryInfo> tmp = new List<CategoryInfo>(cats);
						tmp.Remove(tmp[i]);
						DumpCategories(tmp.ToArray());
						categoriesCache = null;
						return true;
					}
				}
			}
			return false;
		}

		/// <summary>
		/// Merges two Categories.
		/// </summary>
		/// <param name="source">The source Category.</param>
		/// <param name="destination">The destination Category.</param>
		/// <returns>True if the Categories have been merged successfully.</returns>
		/// <remarks>The <b>destination</b> Category remains, while the <b>source</b> Category is deleted, and all its Pages re-binded in the <b>destination</b> Category.</remarks>
		public CategoryInfo MergeCategories(CategoryInfo source, CategoryInfo destination) {
			lock(locker) {
				CategoryInfo[] cats = AllCategories;
				int idx1 = -1, idx2 = -1;
				CategoryNameComparer comp = new CategoryNameComparer();
				for(int i = 0; i < cats.Length; i++) {
					if(comp.Compare(cats[i], source) == 0) idx1 = i;
					if(comp.Compare(cats[i], destination) == 0) idx2 = i;
					if(idx1 != -1 && idx2 != -1) break;
				}
				if(idx1 == -1 || idx2 == -1) return null;
				List<CategoryInfo> tmp = new List<CategoryInfo>(cats);
				List<string> newPages = new List<string>(cats[idx2].Pages);
				for(int i = 0; i < cats[idx1].Pages.Length; i++) {
					bool found = false;
					for(int k = 0; k < newPages.Count; k++) {
						if(newPages[k].ToLower(CultureInfo.CurrentCulture).Equals(cats[idx1].Pages[i].ToLower(CultureInfo.CurrentCulture))) {
							found = true;
							break;
						}
					}
					if(!found) {
						newPages.Add(cats[idx1].Pages[i]);
					}
				}
				tmp[idx2].Pages = newPages.ToArray();
				tmp.Remove(tmp[idx1]);
				DumpCategories(tmp.ToArray());
				CategoryInfo newCat = new CategoryInfo(destination.Name, this);
				newCat.Pages = newPages.ToArray();
				categoriesCache = null;
				return newCat;
			}
		}

		/// <summary>
		/// Gets the Content of a Page.
		/// </summary>
		/// <param name="page">The Page to get the content of.</param>
		/// <returns>The Page Content.</returns>
		public PageContent GetContent(PageInfo page) {
			if(page == null) return null;
			return ExtractContent(Tools.LoadFile(Settings.PagesDirectory + ((LocalPageInfo)page).File), page);
		}

		private PageContent ExtractContent(string data, PageInfo pageInfo) {
			if(data == null) return null;
			// Structure
			// Page Title
			// Username|DateTime[|Comment] --- The comment is optional
			// ##PAGE##
			// Content...
			data = data.Replace("\r", "");
			string[] lines = data.Split('\n');
			string[] fields = lines[1].Split('|');
			int nlIndex = data.IndexOf("\n"); // Index of first new-line char
			// Don't consider page title, since it might contain "##PAGE##"
			int idx = data.Substring(nlIndex + 1).IndexOf("##PAGE##") + 8 + 1 + nlIndex + 1;
			return new PageContent(pageInfo, lines[0], fields[0], DateTime.Parse(fields[1]), fields.Length == 3 ? Tools.UnescapeString(fields[2]) : "", data.Substring(idx));
		}

		/// <summary>
		/// Backups a Page.
		/// </summary>
		/// <param name="page">The Page to backup.</param>
		/// <returns>True if the Page has been backupped successfully.</returns>
		public bool Backup(PageInfo page) {
			List<int> backups = GetBackups(page);
			LocalPageInfo pg = (LocalPageInfo)page;
			int rev = (backups.Count > 0 ? backups[backups.Count - 1] + 1 : 0);
			File.Copy(Settings.PagesDirectory + pg.File,
				Settings.PagesDirectory + Path.GetFileNameWithoutExtension(pg.File) + "." + Tools.GetVersionString(rev) + Path.GetExtension(pg.File));
			return true;
		}

		/// <summary>
		/// Gets the Backup/Revision numbers of a Page.
		/// </summary>
		/// <param name="page">The Page to get the Backup of.</param>
		/// <returns>The list of Backup/Revision numbers.</returns>
		public List<int> GetBackups(PageInfo page) {
			string[] files = Directory.GetFiles(Settings.PagesDirectory, Path.GetFileNameWithoutExtension(((LocalPageInfo)page).File) + ".*.cs");
			List<int> result = new List<int>();
			for(int i = 0; i < files.Length; i++) {
				string num = Path.GetFileNameWithoutExtension(files[i]).Substring(page.Name.Length + 1);
				try {
					// If num is not actually a number, this would crash
					result.Add(int.Parse(num));
				}
				catch { }
			}
			return result;
		}

		/// <summary>
		/// Gets the Content of a Backup.
		/// </summary>
		/// <param name="page">The Page.</param>
		/// <param name="revision">The revision.</param>
		/// <returns>The content.</returns>
		public PageContent GetBackupContent(PageInfo page, int revision) {
			if(!PageExists(page)) return null;
			LocalPageInfo pg = (LocalPageInfo)page;
			string filename = Path.GetFileNameWithoutExtension(pg.File) + "." + Tools.GetVersionString(revision) + Path.GetExtension(pg.File);
			return ExtractContent(Tools.LoadFile(Settings.PagesDirectory + filename), page);
		}

		/// <summary>
		/// Forces to overwrite or create a Backup.
		/// </summary>
		/// <param name="content">The Backup content.</param>
		/// <param name="revision">The revision.</param>
		/// <returns>True if the Backup has been created successfully.</returns>
		public bool SetBackupContent(PageContent content, int revision) {
			StringBuilder sb = new StringBuilder();
			sb.Append(content.Title);
			sb.Append("\r\n");
			sb.Append(content.User);
			sb.Append("|");
			sb.Append(content.LastModified.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss"));
			if(!string.IsNullOrEmpty(content.Comment)) {
				sb.Append("|");
				sb.Append(Tools.EscapeString(content.Comment));
			}
			sb.Append("\r\n##PAGE##\r\n");
			sb.Append(content.Content);
			string filename = "";
			if(content.PageInfo is LocalPageInfo) {
				// Use local file
				LocalPageInfo pg = (LocalPageInfo)content.PageInfo;
				filename = Path.GetFileNameWithoutExtension(pg.File) + "." + Tools.GetVersionString(revision) + Path.GetExtension(pg.File);
			}
			else {
				filename = content.PageInfo.Name + "." + Tools.GetVersionString(revision) + ".cs";
			}
			Tools.WriteFile(Settings.PagesDirectory + filename, sb.ToString());
			return true;
		}

		/// <summary>
		/// Adds a new Page.
		/// </summary>
		/// <param name="name">The Name of the Page.</param>
		/// <param name="creationDateTime">The creation Date/Time.</param>
		/// <returns>The correct PageInfo object.</returns>
		public PageInfo AddPage(string name, DateTime creationDateTime) {
			if(PageExists(new PageInfo(name, this, PageStatus.Normal, DateTime.Now))) return null;
			lock(locker) {
				BackupPagesFile();
				Tools.AppendFile(Settings.PagesFile, "\r\n" + name + "|" + name + ".cs" + "|NORMAL|" + creationDateTime.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss"));
				File.Create(Settings.PagesDirectory + name + ".cs").Close();
				pagesCache = null;
			}
			return new LocalPageInfo(name, this, PageStatus.Normal, creationDateTime, name + ".cs");
		}

		/// <summary>
		/// Renames a Page.
		/// </summary>
		/// <param name="page">The Page to rename.</param>
		/// <param name="newName">The new Name.</param>
		/// <returns>True if the Page has been renamed successfully.</returns>
		public PageInfo RenamePage(PageInfo page, string newName) {
			if(PageExists(new PageInfo(newName, this, PageStatus.Normal, DateTime.Now))) return null;

			lock(locker) {
				PageInfo[] pgs = AllPages;
				bool found = false;
				int idx = -1;
				PageNameComparer comp = new PageNameComparer();
				// Store page's categoriesCache for rebind with new page
				CategoryInfo[] tmp = Pages.Instance.GetCategoriesPerPage(page);
				string[] cats = new string[tmp.Length];
				for(int i = 0; i < tmp.Length; i++) {
					cats[i] = tmp[i].Name;
				}
				// Remove all bindings for old page to avoid Cats file corruption
				Rebind(page, new string[0]);

				// Find page and rename files
				for(int i = 0; i < pgs.Length; i++) {
					if(comp.Compare(pgs[i], page) == 0) {
						pgs[i].Name = newName;
						File.Copy(Settings.PagesDirectory + ((LocalPageInfo)pgs[i]).File,
							Settings.PagesDirectory + newName + Path.GetExtension(((LocalPageInfo)pgs[i]).File));
						File.Delete(Settings.PagesDirectory + ((LocalPageInfo)pgs[i]).File);
						((LocalPageInfo)pgs[i]).File = newName + Path.GetExtension(((LocalPageInfo)pgs[i]).File);
						found = true;
						idx = i;
						break;
					}
				}
				if(found) {
					// Store new page list on disk, rename all backups
					// and rebind new page with old categoriesCache
					DumpPages(pgs);
					RenameBackups(page, newName);
					Rebind(pgs[idx], cats);
					// Clear internal cache
					categoriesCache = null;
					PageInfo result = pgs[idx];
					pagesCache = null;
					return result;
				}
				else return null;
			}
		}

		private void RenameBackups(PageInfo page, string newName) {
			List<int> backups = GetBackups(page);
			LocalPageInfo pg = (LocalPageInfo)page;
			for(int i = 0; i < backups.Count; i++) {
				File.Copy(Settings.PagesDirectory + Path.GetFileNameWithoutExtension(pg.File) + "." + Tools.GetVersionString(backups[i]).ToString() + Path.GetExtension(pg.File),
					Settings.PagesDirectory + newName + "." + Tools.GetVersionString(backups[i]).ToString() + Path.GetExtension(pg.File), false);
				File.Delete(Settings.PagesDirectory + Path.GetFileNameWithoutExtension(pg.File) + "." + Tools.GetVersionString(backups[i]).ToString() + Path.GetExtension(pg.File));
			}
		}

		/// <summary>
		/// Sets the Status of a Page.
		/// </summary>
		/// <param name="page">The Page to set the Status of.</param>
		/// <param name="status">The Status.</param>
		/// <returns>True if the Status has been set successfully.</returns>
		public PageInfo SetStatus(PageInfo page, PageStatus status) 
        {
			lock(locker)
            {
				PageInfo[] pgs = AllPages;
				PageNameComparer comp = new PageNameComparer();
				for(int i = 0; i < pgs.Length; i++) {
					if(comp.Compare(pgs[i], page) == 0) {
						PageInfo tmp = new LocalPageInfo(page.Name, this, status, page.CreationDateTime, ((LocalPageInfo)page).File);
						pgs[i] = tmp;
						DumpPages(pgs);
						return tmp;
					}
				}
			}
			return null;
		}

		/// <summary>
		/// Modifies the Content of a Page.
		/// </summary>
		/// <param name="page">The Page.</param>
		/// <param name="title">The Title of the Page.</param>
		/// <param name="username">The Username.</param>
		/// <param name="dateTime">The Date/Time.</param>
		/// <param name="comment">The comment of the editor, about this revision.</param>
		/// <param name="content">The Page Content.</param>
		/// <param name="backup">A value specifying whether or not to create a backup of the Page.</param>
		/// <returns>True if the Page has been modified successfully.</returns>
		public bool ModifyPage(PageInfo page, string title, string username, DateTime dateTime, string comment, string content, bool backup)
        {
			if(!PageExists(page)) return false;

			if(backup) Backup(page);

			StringBuilder sb = new StringBuilder();
			sb.Append(title);
			sb.Append("\r\n");
			sb.Append(username);
			sb.Append("|");
			sb.Append(dateTime.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss"));
			if(comment.Length > 0) {
				sb.Append("|");
				sb.Append(Tools.EscapeString(comment));
			}
			sb.Append("\r\n##PAGE##\r\n");
			sb.Append(content);

			Tools.WriteFile(Settings.PagesDirectory + ((LocalPageInfo)page).File, sb.ToString());
			return true;
		}

		/// <summary>
		/// Performs the rollback of a Page to a specified revision.
		/// </summary>
		/// <param name="page">The Page to rollback.</param>
		/// <param name="revision">The Revision to rollback the Page to.</param>
		/// <returns>True if the roolback succeeded.</returns>
		/// <remarks>All the newer backups are deleted.</remarks>
		public bool RollbackPage(PageInfo page, int revision)
        {
			if(!PageExists(page)) return false;

			List<int> backups = GetBackups(page);
			if(!backups.Contains(revision)) return false;

			// Operations
			// - Copy the content of the specified revision into the page file
			// - Delete specified revision and all the newer revisions
			
			LocalPageInfo pg = (LocalPageInfo)page;
			File.Copy(Settings.PagesDirectory + Path.GetFileNameWithoutExtension(pg.File) + "." + Tools.GetVersionString(revision) + Path.GetExtension(pg.File),
				Settings.PagesDirectory + pg.File, true);

			int idx = backups.IndexOf(revision);

			for(int i = backups.Count - 1; i >= idx; i--) {
				File.Delete(Settings.PagesDirectory + Path.GetFileNameWithoutExtension(pg.File) + "." + Tools.GetVersionString(backups[i]) + Path.GetExtension(pg.File));
			}

			return true;
		}

		/// <summary>
		/// Deletes the Backups of a Page, up to a specified revision.
		/// </summary>
		/// <param name="page">The Page to delete the backups of.</param>
		/// <param name="revision">The newest revision to delete (newer revision are kept) or -1 to delete all the Backups.</param>
		/// <returns>True if the deletion succeeded.</returns>
		public bool DeleteBackups(PageInfo page, int revision) {
			lock(locker) {
				List<int> backups = GetBackups(page);
				if(backups.Count == 0) return true;

				int idx = (revision != -1 ? backups.IndexOf(revision) : backups[backups.Count - 1]);

				// Operations
				// - Delete old beckups, from 0 to revision
				// - Rename newer backups starting from 0

				LocalPageInfo pg = (LocalPageInfo)page;

				for(int i = 0; i <= idx; i++) {
					File.Delete(Settings.PagesDirectory + Path.GetFileNameWithoutExtension(pg.File) + "." + Tools.GetVersionString(backups[i]) + Path.GetExtension(pg.File));
				}

				if(revision != -1) {
					for(int i = revision + 1; i < backups.Count; i++) {
						File.Copy(Settings.PagesDirectory + Path.GetFileNameWithoutExtension(pg.File) + "." + Tools.GetVersionString(backups[i]) + Path.GetExtension(pg.File),
							Settings.PagesDirectory + Path.GetFileNameWithoutExtension(pg.File) + "." + Tools.GetVersionString(backups[i] - revision - 1) + Path.GetExtension(pg.File));
						File.Delete(Settings.PagesDirectory + Path.GetFileNameWithoutExtension(pg.File) + "." + Tools.GetVersionString(backups[i]) + Path.GetExtension(pg.File));
					}
				}
			}
			return true;
		}

		/// <summary>
		/// Removes a Page.
		/// </summary>
		/// <param name="page">The Page to remove.</param>
		/// <returns>True if the Page has been removed successfully.</returns>
		public bool RemovePage(PageInfo page) {
			lock(locker) {
				List<PageInfo> pgs = new List<PageInfo>(AllPages);
				PageNameComparer comp = new PageNameComparer();
				for(int i = 0; i < pgs.Count; i++) {
					if(comp.Compare(pgs[i], page) == 0) {
						pgs.Remove(pgs[i]);
						DumpPages(pgs.ToArray());
						DeleteBackups(page, -1);
						try {
							File.Delete(Settings.PagesDirectory + ((LocalPageInfo)page).File);
							File.Delete(Settings.MessagesDirectory + ((LocalPageInfo)page).File);
						}
						catch { }
						pagesCache = null;
						categoriesCache = null;
						return true;
					}
				}
			}
			return false;
		}

		/// <summary>
		/// Binds a Page with one or more Categories.
		/// </summary>
		/// <param name="page">The Page to bind.</param>
		/// <param name="categories">The Categories to bind the Page with.</param>
		/// <returns>True if the binding succeeded.</returns>
		/// <remarks>After a successful operation, the Page is binded with all and only the categoriesCache passed as argument.</remarks>
		public bool Rebind(PageInfo page, string[] categories) {
			lock(locker) {
				if(!PageExists(page)) return false;

				CategoryInfo[] cats = AllCategories;

				// Operations:
				// - Remove the Page from every Category
				// - For each specified category, add (if needed) the Page
				List<string> pages;
				CategoryNameComparer catComp = new CategoryNameComparer();
				for(int i = 0; i < cats.Length; i++) {
					pages = new List<string>(cats[i].Pages);
					int idx = Contains(pages, page.Name);
					if(idx != -1) pages.Remove(pages[idx]);
					cats[i].Pages = pages.ToArray();
				}
				for(int i = 0; i < cats.Length; i++) {
					for(int k = 0; k < categories.Length; k++) {
						if(catComp.Compare(cats[i], new CategoryInfo(categories[k], this)) == 0) {
							pages = new List<string>(cats[i].Pages);
							pages.Add(page.Name);
							cats[i].Pages = pages.ToArray();
						}
					}
				}
				DumpCategories(cats);
				pagesCache = null;
				categoriesCache = null;
			}
			return true;
		}

		private int Contains(List<string> pages, string page) {
			for(int i = 0; i < pages.Count; i++) {
				if(StringComparer.OrdinalIgnoreCase.Compare(pages[i], page) == 0) return i;
			}
			return -1;
		}

		private void BackupPagesFile() {
			File.Copy(Settings.PagesFile,
				Settings.PublicDirectory + Path.GetFileNameWithoutExtension(Settings.PagesFile) +
				".bak" + Path.GetExtension(Settings.PagesFile), true);
		}

		private void DumpPages(PageInfo[] pages) {
			BackupPagesFile();

			StringBuilder sb = new StringBuilder();
			for(int i = 0; i < pages.Length; i++) {
				sb.Append(pages[i].Name);
				sb.Append("|");
				sb.Append(((LocalPageInfo)pages[i]).File);
				switch(pages[i].Status) {
					case PageStatus.Normal:
						sb.Append("|NORMAL");
						break;
					case PageStatus.Locked:
						sb.Append("|LOCKED");
						break;
					case PageStatus.Public:
						sb.Append("|PUBLIC");
						break;
				}
				sb.Append("|");
				sb.Append(pages[i].CreationDateTime.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss"));
				if(i != pages.Length - 1) sb.Append("\r\n");
			}
			Tools.WriteFile(Settings.PagesFile, sb.ToString());
		}

		private void BackupCategoriesFile() {
			File.Copy(Settings.CategoriesFile,
				Settings.PublicDirectory + Path.GetFileNameWithoutExtension(Settings.CategoriesFile) +
				".bak" + Path.GetExtension(Settings.CategoriesFile), true);
		}

		private void DumpCategories(CategoryInfo[] categories) {
			BackupCategoriesFile();

			StringBuilder sb = new StringBuilder();
			for(int i = 0; i < categories.Length; i++) {
				sb.Append(categories[i].Name);
				if(categories[i].Pages.Length > 0) {
					for(int k = 0; k < categories[i].Pages.Length; k++) {
						sb.Append("|");
						sb.Append(categories[i].Pages[k]);
					}
				}
				sb.Append("\r\n");
			}
			Tools.WriteFile(Settings.CategoriesFile, sb.ToString());
		}

		/// <summary>
		/// Gets the Page Messages.
		/// </summary>
		/// <param name="page">The Page.</param>
		/// <returns>The list of the <b>first-level</b> Messages, containing the replies properly nested.</returns>
		public Message[] GetMessages(PageInfo page) {
			LocalPageInfo pg = (LocalPageInfo)page;

			// Shortcut
			if(!File.Exists(Settings.MessagesDirectory + pg.File)) return new Message[0];

			string data;
			lock(lockerDisc) {
				data = Tools.LoadFile(Settings.MessagesDirectory + pg.File).Replace("\r", "");
			}

			string[] lines = data.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);

			List<Message> result = new List<Message>();

			// Structure
			// ID|Username|Subject|DateTime|ParentID|Body

			// This algorithm DOES not handle replies that are stored BEFORE their parent,
			// so every reply MUST be stored anywhere but AFTER its parent
			// (the message tree should be stored depht-first; new messages can be appended at the end of the file)

			string[] fields;
			int id, parent;
			string username, subject, body;
			DateTime dateTime;
			for(int i = 0; i < lines.Length; i++) {
				fields = lines[i].Split('|');
				id = int.Parse(fields[0]);
				username = fields[1];
				subject = Tools.UnescapeString(fields[2]);
				dateTime = DateTime.Parse(fields[3]);
				parent = int.Parse(fields[4]);
				body = Tools.UnescapeString(fields[5]);
				if(parent != -1) {
					// Find parent
					Message p = FindMessage(result, parent);
					if(p == null) {
						// Add as top-level message
						result.Add(new Message(id, username, subject, dateTime, body));
					}
					else {
						// Add to parent's replies
						p.Replies.Add(new Message(id, username, subject, dateTime, body));
					}
				}
				else {
					// Add as top-level message
					result.Add(new Message(id, username, subject, dateTime, body));
				}
			}

			return result.ToArray();
		}

		/// <summary>
		/// Gets the total number of Messages in a Page Discussion.
		/// </summary>
		/// <param name="page">The Page.</param>
		/// <returns>The number of messages.</returns>
		public int GetMessageCount(PageInfo page) {
			LocalPageInfo pg = (LocalPageInfo)page;
			if(!File.Exists(Settings.MessagesDirectory + pg.File)) return 0;
			string data = Tools.LoadFile(Settings.MessagesDirectory + pg.File).Replace("\r", "");
			return data.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries).Length;
		}

		/// <summary>
		/// Finds a Message in a Message tree.
		/// </summary>
		/// <param name="messages">The Message tree.</param>
		/// <param name="id">The ID of the Message to find.</param>
		/// <returns>The Message or null.</returns>
		/// <remarks>The method is recursive.</remarks>
		private Message FindMessage(List<Message> messages, int id) {
			Message result = null;
			for(int i = 0; i < messages.Count; i++) {
				if(messages[i].ID == id) {
					result = messages[i];
				}
				if(result == null) {
					result = FindMessage(messages[i].Replies, id);
				}
				if(result != null) break;
			}
			return result;
		}

		/// <summary>
		/// Adds a new Message to a Page.
		/// </summary>
		/// <param name="page">The Page.</param>
		/// <param name="username">The Username.</param>
		/// <param name="subject">The Subject.</param>
		/// <param name="dateTime">The Date/Time.</param>
		/// <param name="body">The Body.</param>
		/// <param name="parent">The Parent Message ID, or -1.</param>
		/// <returns>True if the Message has been added successfully.</returns>
		public bool AddMessage(PageInfo page, string username, string subject, DateTime dateTime, string body, int parent) {
			subject = Tools.EscapeString(subject);
			body = Tools.EscapeString(body);
			StringBuilder sb = new StringBuilder();

			// Structure
			// ID|Username|Subject|DateTime|ParentID|Body

			sb.Append(GetFreeMessageID(page));
			sb.Append("|");
			sb.Append(username);
			sb.Append("|");
			sb.Append(subject);
			sb.Append("|");
			sb.Append(dateTime.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss"));
			sb.Append("|");
			sb.Append(parent.ToString());
			sb.Append("|");
			sb.Append(body);
			sb.Append("\r\n");

			Tools.AppendFile(Settings.MessagesDirectory + ((LocalPageInfo)page).File, sb.ToString());

			return true;
		}

		/// <summary>
		/// Find a free Message ID for a Page.
		/// </summary>
		/// <param name="page">The Page.</param>
		/// <returns>The Message ID.</returns>
		private int GetFreeMessageID(PageInfo page) {
			LocalPageInfo pg = (LocalPageInfo)page;

			if(!File.Exists(Settings.MessagesDirectory + pg.File)) return 0;

			int result = 0;

			string data;
			lock(lockerDisc) {
				data = Tools.LoadFile(Settings.MessagesDirectory + pg.File).Replace("\r", "");
			}

			string[] lines = data.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
			int idx, tmp;
			for(int i = 0; i < lines.Length; i++) {
				idx = lines[i].IndexOf('|');
				tmp = int.Parse(lines[i].Substring(0, idx));
				if(tmp > result) result = tmp;
			}

			result++;

			return result;
		}

		/// <summary>
		/// Removes a Message.
		/// </summary>
		/// <param name="page">The Page.</param>
		/// <param name="id">The ID of the Message to remove.</param>
		/// <param name="removeReplies">A value specifying whether or not to remove the replies.</param>
		/// <returns>True if the Message has been removed successfully.</returns>
		public bool RemoveMessage(PageInfo page, int id, bool removeReplies) {
			List<Message> messages = new List<Message>(GetMessages(page));
			Message msg = FindMessage(messages, id);
			if(msg == null) return false;

			List<Message> replies = null;
			if(!removeReplies) {
				replies = msg.Replies;
			}

			if(!removeReplies && replies.Count > 0) {
				// Find Message's anchestor
				Message anchestor = FindAnchestor(messages, msg.ID);
				if(anchestor != null) anchestor.Replies.AddRange(replies);
				else messages.AddRange(replies);
			}

			RemoveMessage(messages, msg);

			lock(lockerDisc) {
				DumpMessages(page, messages);
			}

			return true;
		}

		/// <summary>
		/// Finds the anchestor/parent of a Message.
		/// </summary>
		/// <param name="messages">The Messages.</param>
		/// <param name="id">The Message ID.</param>
		/// <returns>The anchestor Message or null.</returns>
		private Message FindAnchestor(List<Message> messages, int id) {
			Message result = null;
			for(int i = 0; i < messages.Count; i++) {
				for(int k = 0; k < messages[i].Replies.Count; k++) {
					if(messages[i].Replies[k].ID == id) {
						result = messages[i];
						break;
					}
					if(result == null) {
						result = FindAnchestor(messages[i].Replies, id);
					}
				}
				if(result != null) break;
			}
			return result;
		}

		/// <summary>
		/// Removes a Message from a Message Tree.
		/// </summary>
		/// <param name="messages">The Message Tree.</param>
		/// <param name="msg">The Message to Remove.</param>
		/// <returns>True if the Message has been removed.</returns>
		private bool RemoveMessage(List<Message> messages, Message msg) {
			for(int i = 0; i < messages.Count; i++) {
				if(messages.Contains(msg)) {
					messages.Remove(msg);
					return true;
				}
				bool done = RemoveMessage(messages[i].Replies, msg);
				if(done) return true;
			}
			return false;
		}

		/// <summary>
		/// Modifies a Message.
		/// </summary>
		/// <param name="page">The Page.</param>
		/// <param name="id">The ID of the Message to modify.</param>
		/// <param name="username">The Username.</param>
		/// <param name="subject">The Subject.</param>
		/// <param name="dateTime">The Date/Time.</param>
		/// <param name="body">The Body.</param>
		/// <returns>True if the Message has been modified successfully.</returns>
		public bool ModifyMessage(PageInfo page, int id, string username, string subject, DateTime dateTime, string body) {
			List<Message> messages = new List<Message>(GetMessages(page));

			Message msg = FindMessage(messages, id);
			
			if(msg == null) return false;

			msg.Username = username;
			msg.Subject = subject;
			msg.DateTime = dateTime;
			msg.Body = body;

			lock(lockerDisc) {
				DumpMessages(page, messages);
			}

			return true;
		}

		/// <summary>
		/// Dumps the Message tree of a Page to disk.
		/// </summary>
		/// <param name="page">The Page.</param>
		/// <param name="messages">The Message tree.</param>
		private void DumpMessages(PageInfo page, List<Message> messages) {
			StringBuilder sb = new StringBuilder();
			AppendMessages(messages, -1, sb);
			Tools.WriteFile(Settings.MessagesDirectory + ((LocalPageInfo)page).File, sb.ToString());
		}

		/// <summary>
		/// Appends to a StringBuilder object the branches and leaves of a Message tree.
		/// </summary>
		/// <param name="messages">The Message tree branch to append.</param>
		/// <param name="parent">The ID of the parent of the Message tree or -1.</param>
		/// <param name="sb">The StringBuilder.</param>
		/// <remarks>The methods appends the Messages traversing the tree depht-first, and it is recursive.</remarks>
		private void AppendMessages(List<Message> messages, int parent, StringBuilder sb) {
			// Depht-first

			// Structure
			// ID|Username|Subject|DateTime|ParentID|Body

			for(int i = 0; i < messages.Count; i++) {
				sb.Append(messages[i].ID.ToString());
				sb.Append("|");
				sb.Append(messages[i].Username);
				sb.Append("|");
				sb.Append(Tools.EscapeString(messages[i].Subject));
				sb.Append("|");
				sb.Append(messages[i].DateTime.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss"));
				sb.Append("|");
				sb.Append(parent.ToString());
				sb.Append("|");
				sb.Append(Tools.EscapeString(messages[i].Body));
				sb.Append("\r\n");
				AppendMessages(messages[i].Replies, messages[i].ID, sb);
			}
		}

		/// <summary>
		/// Gets all the Navigation Paths.
		/// </summary>
		/// <remarks>The array is unsorted.</remarks>
		public NavigationPath[] AllNavigationPaths {
			get {
				List<NavigationPath> paths = new List<NavigationPath>();

				string[] lines = Tools.LoadFile(Settings.NavigationPathsFile).Replace("\r", "").Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);

				// Structure
				// PathName|Page1|Page2|...

				string[] fields;
				for(int i = 0; i < lines.Length; i++) {
					fields = lines[i].Split('|');
					if(fields.Length == 0) continue;
					NavigationPath p = new NavigationPath(fields[0], this);
					for(int k = 1; k < fields.Length; k++) {
						p.Pages.Add(fields[k]);
					}
					paths.Add(p);
				}
				return paths.ToArray();
			}
		}

		/// <summary>
		/// Adds a new Navigation Path.
		/// </summary>
		/// <param name="name">The Name of the Path.</param>
		/// <param name="pages">The Pages array.</param>
		/// <returns>True if the new Path is added successfully.</returns>
		public NavigationPath AddNavigationPath(string name, PageInfo[] pages) {
			StringBuilder sb = new StringBuilder();
			NavigationPath tmp = new NavigationPath(name, this);
			sb.Append("\r\n");
			sb.Append(name);
			for(int i = 0; i < pages.Length; i++) {
				if(pages[i].Provider == this) {
					sb.Append("|");
					sb.Append(pages[i].Name);
					tmp.Pages.Add(pages[i].Name);
				}
			}
			Tools.AppendFile(Settings.NavigationPathsFile, sb.ToString());
			return tmp;
		}

		/// <summary>
		/// Modifies an existing Navigation Path.
		/// </summary>
		/// <param name="name">The Name of the Path to modify.</param>
		/// <param name="pages">The Pages array.</param>
		/// <returns>True if the Path is modified successfully.</returns>
		public NavigationPath ModifyNavigationPath(string name, PageInfo[] pages) {
			NavigationPath[] paths = AllNavigationPaths;
			NavigationPathComparer comp = new NavigationPathComparer();
			NavigationPath tmp = new NavigationPath(name, this);
			for(int i = 0; i < paths.Length; i++) {
				if(comp.Compare(tmp, paths[i]) == 0) {
					paths[i].Pages.Clear();
					NavigationPath np = new NavigationPath(name, this);
					for(int k = 0; k < pages.Length; k++) {
						if(pages[i].Provider == this) {
							np.Pages.Add(pages[k].Name);
						}
					}
					paths[i] = np;
					DumpNavigationPaths(paths);
					return np;
				}
			}
			return null;
		}

		/// <summary>
		/// Removes a Navigation Path.
		/// </summary>
		/// <param name="name">The Name of the Path to remove.</param>
		/// <returns>True if the Path is removed successfully.</returns>
		public bool RemoveNavigationPath(string name) {
			List<NavigationPath> paths = new List<NavigationPath>(AllNavigationPaths);
			NavigationPathComparer comp = new NavigationPathComparer();
			NavigationPath tmp = new NavigationPath(name, this);
			for(int i = 0; i < paths.Count; i++) {
				if(comp.Compare(tmp, paths[i]) == 0) {
					paths.Remove(paths[i]);
					DumpNavigationPaths(paths.ToArray());
					return true;
				}
			}
			return false;
		}

		/// <summary>
		/// Writes an array of Navigation Paths to disk.
		/// </summary>
		/// <param name="paths">The array.</param>
		private void DumpNavigationPaths(NavigationPath[] paths) {
			StringBuilder sb = new StringBuilder();
			for(int i = 0; i < paths.Length; i++) {
				sb.Append(paths[i].Name);
				for(int k = 0; k < paths[i].Pages.Count; k++) {
					sb.Append("|");
					sb.Append(paths[i].Pages[k]);
				}
				if(i != paths.Length - 1) sb.Append("\r\n");
			}
			Tools.WriteFile(Settings.NavigationPathsFile, sb.ToString());
		}

		/// <summary>
		/// Gets all the Snippets.
		/// </summary>
		public Snippet[] AllSnippets {
			get {
				List<Snippet> snippets = new List<Snippet>();
				string[] files = Directory.GetFiles(Settings.SnippetsDirectory, "*.cs");
				for(int i = 0; i < files.Length; i++) {
					snippets.Add(new Snippet(Path.GetFileNameWithoutExtension(files[i]), Tools.LoadFile(files[i]), this));
				}
				return snippets.ToArray();
			}
		}

		/// <summary>
		/// Adds a new Snippet.
		/// </summary>
		/// <param name="name">The Name of the Snippet.</param>
		/// <param name="content">The Content of the Snippet.</param>
		/// <returns>The correct Snippet object.</returns>
		public Snippet AddSnippet(string name, string content) {
			Tools.WriteFile(Settings.SnippetsDirectory + name + ".cs", content);
			return new Snippet(name, content, this);
		}

		/// <summary>
		/// Modifies a new Snippet.
		/// </summary>
		/// <param name="name">The Name of the Snippet to modify.</param>
		/// <param name="content">The Content of the Snippet.</param>
		/// <returns>The correct Snippet object.</returns>
		public Snippet ModifySnippet(string name, string content) {
			Tools.WriteFile(Settings.SnippetsDirectory + name + ".cs", content);
			return new Snippet(name, content, this);
		}

		/// <summary>
		/// Removes a new Snippet.
		/// </summary>
		/// <param name="name">The Name of the Snippet to remove.</param>
		/// <returns>True if the Snippet is removed successfully.</returns>
		public bool RemoveSnippet(string name) {
			File.Delete(Settings.SnippetsDirectory + name + ".cs");
			return true;
		}

	}

}
